import { computed } from 'vue'
import {
	isFunction,
	empty,
	isEqual,
	getProperty
} from "../"
import type {NodeValue,NodePathValue,Option,Config,Props,Nullable} from './type'
export * from "./type";



type ChildrenData = Option[] | undefined



let uid = 0

const calculatePathNodes = (node: Node) => {
	const nodes = [node]
	let { parent } = node

	while (parent) {
		nodes.unshift(parent)
		parent = parent.parent
	}

	return nodes
}


const resetChildCheck = (nodes:Node[]) => {
		nodes.forEach(item => item.doCheck(false))
	}

export class Node {
	readonly uid: number = uid++
	readonly level: number
	readonly value: NodeValue
	readonly label: string
	readonly pathNodes: Node[]
	readonly pathValues: NodePathValue
	readonly pathLabels: string[]

	childrenData: ChildrenData
	children: Node[]
	text: string
	loaded: boolean
	checked = false
	indeterminate = false
	loading = false

	constructor(
		readonly data: Nullable<Option>,
		readonly config: Config,
		readonly parent?: Node,
		readonly root = false
	) {
		const { value: valueKey, label: labelKey, children: childrenKey } = config
		
		const childrenData = getProperty(data,childrenKey) as ChildrenData
		const pathNodes = calculatePathNodes(this)

		this.level = root ? 0 : parent ? parent.level + 1 : 1
		this.value = getProperty(data,valueKey) as NodeValue
		this.label = getProperty(data,labelKey)as string
		this.pathNodes = pathNodes
		this.pathValues = pathNodes.map((node) => node.value)
		this.pathLabels = pathNodes.map((node) => node.label)
		this.childrenData = childrenData
		this.children = (childrenData || []).map(
			(child) => new Node(child, config, this)
		)
		this.loaded = !config.lazy  || !empty(childrenData)
	}

	get isDisabled(): boolean {
		const { data, parent, config } = this
		const { disabled } = config
		const isDisabled = isFunction(disabled)
			? disabled(data, this)
			: !!getProperty(data,disabled)
		return isDisabled || (parent?.isDisabled)
	}


	// get valueByOption() {
	// 	return this.config.emitPath ? this.pathValues : this.value
	// }

	appendChild(childData: Option) {
		const { childrenData, children } = this
		const node = new Node(childData, this.config, this)

		if (Array.isArray(childrenData)) {
			childrenData.push(childData)
		} else {
			this.childrenData = [childData]
		}

		children.push(node)

		return node
	}

	calcText(allLevels: boolean, separator: string) {
		const text = allLevels ? this.pathLabels.join(separator) : this.label
		this.text = text
		return text
	}

	broadcast(event: string, ...args: unknown[]) {
		const handlerName = `onParent${event}`
		this.children.forEach((child) => {
			if (child) {
				// bottom up
				child.broadcast(event, ...args)
				child[handlerName] && child[handlerName](...args)
			}
		})
	}

	emit(event: string, ...args: unknown[]) {
		const { parent } = this
		const handlerName = `onChild${event}`
		if (parent) {
			parent[handlerName] && parent[handlerName](...args)
			parent.emit(event, ...args)
		}
	}

	onParentCheck(checked: boolean) {
		if (!this.isDisabled) {
			this.setCheckState(checked)
		}
	}

	resetChildCheck() {
		const { children } = this
		resetChildCheck(children)
	}

	setCheckState(checked: boolean) {
		const totalNum = this.children.length
		const checkedNum = this.children.reduce((c, p) => {
			const num = p.checked ? 1 : p.indeterminate ? 0.5 : 0
			return c + num
		}, 0)

		this.checked =
			this.loaded &&
			this.children.every((child) => child.loaded && child.checked) &&
			checked
		this.indeterminate =
			this.loaded && checkedNum !== totalNum && checkedNum > 0
	}

	doCheck(checked: boolean) {
		if (this.checked === checked) return

		const { multiple } = this.config

		if (!multiple) {
			this.checked = checked

		} else {
			// bottom up to unify the calculation of the indeterminate state
			this.broadcast('check', checked)
			this.setCheckState(checked)
			this.emit('check')
		}
	}
}

const flatNodes = (nodes: Node[]) => {
	return nodes.reduce((res, node) => {
		 res.push(node)
		res = res.concat(flatNodes(node.children))
		return res
	}, [] as Node[])
}

export class Store {
	readonly nodes: Node[]
	readonly allNodes: Node[]

	constructor(data: Option[], readonly config: Config) {
		const nodes = (data || []).map(
			(nodeData) => new Node(nodeData, this.config)
		)
		this.nodes = nodes
		this.allNodes = flatNodes(nodes)
	}

	getNodes() {
		return this.nodes
	}

	getFlattedNodes() {
		return this.allNodes
	}

	appendNode(nodeData: Option, parentNode?: Node) {
		const node = parentNode
			? parentNode.appendChild(nodeData)
			: new Node(nodeData, this.config)

		if (!parentNode) this.nodes.push(node)

		this.allNodes.push(node)
	}

	appendNodes(nodeDataList: Option[], parentNode: Node) {
		nodeDataList.forEach((nodeData) => this.appendNode(nodeData, parentNode))
	}
	resetChildCheck() {
		const { nodes } = this
		resetChildCheck(nodes)
	}

	// when checkStrictly, leaf node first
	getNodeByValue(
		value: NodeValue | NodePathValue
	): Nullable<Node> {
		if (!value && value !== 0) return null

		const node = this.getFlattedNodes().find(
			(node) => isEqual(node.value, value) || isEqual(node.pathValues, value)
		)

		return node || null
	}

	getSameNode(node: Node): Nullable<Node> {
		if (!node) return null

		const node_ = this.getFlattedNodes().find(
			({ value, level }) => isEqual(node.value, value) && node.level === level
		)

		return node_ || null
	}
}

export const DefaultProps: Config = {
	multiple: false,
	emitPath: true,
	lazy: false,
	lazyLoad: () => { },
	value: 'value',
	label: 'label',
	children: 'children',
	disabled: 'disabled',
}
export const useLazyLoadConfig = (props: { props?: Props }) => {
	return {
		config: computed(() => ({
			...DefaultProps,
			...props.props,
		}))
	}
}



